﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;
using System.Diagnostics.CodeAnalysis;
using WasmAppBuilder;

using JoinedString;
//
// This class generates the icall_trampoline_dispatch () function used by the interpreter to call native code on WASM.
// It should be kept in sync with mono_wasm_interp_to_native_trampoline () in the runtime.
//

#nullable enable

internal sealed class InterpToNativeGenerator
{
    private LogAdapter Log { get; set; }

    public InterpToNativeGenerator(LogAdapter log) => Log = log;

    public void Generate(IEnumerable<string> cookies, string outputPath)
    {
        using TempFileName tmpFileName = new();
        using (var w = File.CreateText(tmpFileName.Path))
        {
            Emit(w, cookies);
        }

        if (Utils.CopyIfDifferent(tmpFileName.Path, outputPath, useHash: false))
            Log.LogMessage(MessageImportance.Low, $"Generating managed2native table to '{outputPath}'.");
        else
            Log.LogMessage(MessageImportance.Low, $"Managed2native table in {outputPath} is unchanged.");
    }

    private static void Emit(StreamWriter w, IEnumerable<string> cookies)
    {
        // Use OrderBy because Order() is not available on .NET Framework
        var signatures = cookies.OrderBy(c => c).Distinct().ToArray();
        Array.Sort(signatures, StringComparer.Ordinal);


        static IEnumerable<char> Args (string signature)
        {
            for (int i = 1; i < signature.Length; ++i)
                yield return signature[i];
        }

        static (bool isVoid, string nativeType) Result (string signature)
            => new(SignatureMapper.IsVoidSignature(signature), SignatureMapper.CharToNativeType(signature[0]));

        w.Write(
        """
        /*
        * GENERATED FILE, DON'T EDIT
        * Generated by InterpToNativeGenerator
        */

        #include "pinvoke.h"
        #include <stdlib.h>

        """);

        foreach (var signature in signatures)
        {
            try
            {
                var ctx = new EmitCtx();
                var result = Result(signature);
                var args = Args(signature);
                w.Write(
                    $$"""

                    static void
                    wasm_invoke_{{signature.ToLower(CultureInfo.InvariantCulture)}} (void *target_func, MonoInterpMethodArguments *margs)
                    {
                        typedef {{result.nativeType}} (*T)({{args.Join(", ", (p, i) => $"{SignatureMapper.CharToNativeType(p)} arg_{i}")}}{{(signature.Length == 1 ? "void" : "")}});
                        T func = (T)target_func;
                    {{(result.isVoid ?
                    $$""""
                        func ({{args.Join(", ", p => $"{ctx.Emit(p)}")}});
                    """" :
                    $$""""
                        {{result.nativeType}} res = func ({{args.Join(", ", p => $"{ctx.Emit(p)}")}});

                        void *retval = mono_wasm_interp_method_args_get_retval (margs);
                        *({{result.nativeType}} *)retval = res;
                    """")}}
                    }

                    """);
            }
            catch (InvalidSignatureCharException e)
            {
                throw new LogAsErrorException($"Element '{e.Char}' of signature '{signature}' can't be handled by managed2native generator");
            }
        }

        w.Write(
            $$"""

            typedef struct {
                const char* signature;
                void* func;
            } InterpToNative;

            static InterpToNative interp_to_native_invokes [] = {
            {{signatures.Join($",{w.NewLine}", signature =>
            $"    {{\"{signature}\", wasm_invoke_{signature.ToLower(CultureInfo.InvariantCulture)}}}")
            }}
            };

            static int
            compare_signature (const void *key, const void *elem)
            {
                return strcmp (key, ((InterpToNative *)elem)->signature);
            }

            static void*
            mono_wasm_interp_to_native_callback (char* cookie)
            {
                InterpToNative *m2n = bsearch (cookie, interp_to_native_invokes, sizeof(interp_to_native_invokes) / sizeof(InterpToNative), sizeof (InterpToNative), compare_signature);
                if (!m2n)
                    return NULL;
                return m2n->func;
            };
            """);
    }

    private sealed class EmitCtx
    {
        private int iarg, farg;

        public string Emit(char c)
        {
            int argIndex;
            switch (c)
            {
                case 'I':
                    argIndex = iarg;
                    iarg += 1;
                    break;
                case 'L':
                    argIndex = iarg;
                    iarg += 2;
                    break;
                case 'F':
                case 'D':
                    argIndex = farg;
                    farg += 1;
                    break;
                default:
                    throw new InvalidSignatureCharException(c);
            }

            return $"mono_wasm_interp_method_args_get_{char.ToLower(c, CultureInfo.InvariantCulture)}arg (margs, {argIndex})";
        }
    }
}
